#include "./../../../Headers/mathematics/matrix/math_mat_2d.h"

#ifdef ALLOW_DEBUGS
#define DEBUG_MAT_2D
#endif

namespace Mathematics {
	namespace Matrix {
		
			// -static private methods-
			
			// -private methods-
		void Matrix2D::copy(const Matrix2D &original){
		#ifdef DEBUG_MAT_2D
			cout << "Copy: " << id << pendl;
		#endif
			for(register uint i = 0; i < dim_lin; ++i){
				value[i] = original.value[i];
			}
		};
		
		bool Matrix2D::create(){
		#ifdef DEBUG_MAT_2D
			cout << "Create: " << id << pendl;
		#endif
			value = (Matrix1D**) malloc(dim_lin * sizeof(Matrix1D*));
			for(register uint i = 0; i < dim_lin; ++i){
				value[i] = new Matrix1D(dim_col);
			}
			return value != NULL;
		};
		
		void Matrix2D::free(){
		#ifdef DEBUG_MAT_2D
			cout << "Free: ID - " << id << pendl;
		#endif
			if(value != NULL){
				for(register uint i = 0; i < dim_lin; ++i){
					delete value[i];
					value[i] = NULL;
				}
			}
			dim_lin = dim_col = 0;
		};
		
		Matrix2D::Matrix2D():
			Wtb(), id(assignId()),copied(false),dim_lin(0),dim_col(0),value(NULL){
		#ifdef DEBUG_MAT_2D
			debug_function_info( "Hidden Constructor: ", "Matrix2D", (int) &( *this ), sizeof( *this ), id );
			cout << "Line Dimension: " << dim_lin << pendl;
			cout << "Column Dimension: " << dim_col << pendl;
		#endif
		};
		
		Matrix2D::Matrix2D(const uint &line_dimension):
			Wtb(), id(assignId()),copied(false),dim_lin(line_dimension),dim_col(0),value(NULL){
		#ifdef DEBUG_MAT_2D
			debug_function_info( "Hidden Constructor: ", "Matrix2D", (int) &( *this ), sizeof( *this ), id );
			cout << "Line Dimension: " << dim_lin << pendl;
			cout << "Column Dimension: " << dim_col << pendl;
		#endif
			value = (Matrix1D**) malloc(dim_lin * sizeof(Matrix1D*));
		};
		
			// -constructors-
		Matrix2D::Matrix2D(const uint &line_dimension, const uint &column_dimension):
			Wtb(), id(assignId()),copied(false),dim_lin(line_dimension),dim_col(column_dimension),value(NULL){
		#ifdef DEBUG_MAT_2D
			debug_function_info( "Constructor: ", "Matrix2D", (int) &( *this ), sizeof( *this ), id );
			cout << "Line Dimension: " << dim_lin << pendl;
			cout << "Column Dimension: " << dim_col << pendl;
		#endif
			if(!create()){
				cout << endl << "Problem while creating Matrix2D" << endl;
				exit(-1);
			};
		};
		
		Matrix2D::Matrix2D(const Matrix2D &original):
			Wtb(), id(assignId()),copied(false),dim_lin(original.dim_lin),dim_col(original.dim_col),value(NULL){
		#ifdef DEBUG_MAT_2D
			debug_function_info( "Copy Constructor: ", "Matrix2D", (int) &( *this ), sizeof( *this ), id );
			cout << "Line Dimension: " << dim_lin << pendl;
			cout << "Column Dimension: " << dim_col << pendl;
		#endif
			if(!create()){
				cout << endl << "Problem while creating Matrix2D" << endl;
				exit(-1);
			};
			copy(original);
		};
		
		Matrix2D::Matrix2D( const Matrix1D &original ):
			Wtb(), id(assignId()),copied(false),dim_lin(original.getDim()),dim_col(1),value(NULL){
		#ifdef DEBUG_MAT_2D
			debug_function_info( "Copy Constructor: ", "Matrix2D", (int) &( *this ), sizeof( *this ), id );
			cout << "Line Dimension: " << dim_lin << pendl;
			cout << "Column Dimension: " << dim_col << pendl;
		#endif
			if(!create()){
				cout << endl << "Problem while creating Matrix2D" << endl;
				exit(-1);
			};
			for( register usint i = 0; i < dim_lin; ++i ){
				(*value[i])[0] = original[i];
			}
		};
		
		Matrix2D::~Matrix2D(){
		#ifdef DEBUG_MAT_2D
			debug_function_info( "Destructor: ", "Matrix2D", (int) &( *this ), sizeof( *this ), id );
			cout << "Line Dimension: " << dim_lin << pendl;
			cout << "Column Dimension: " << dim_col << pendl;
		#endif
		
			if(!copied){ // alias can't destroy anything
		#ifdef DEBUG_MAT_2D
				cout << "Original." << endl;
		#endif
				free();
			}
		};
		
			// -getters & setters-
		Matrix2D Matrix2D::getSelection(const uint &line_index, const uint &column_index,
			const uint &line_length, const uint &column_length) const {
			if(line_index < 0){
				throw OutOfBoundsException("Invalid access",dim_lin,line_index);
			}
			if(line_index+line_length > dim_lin){
				throw OutOfBoundsException("Invalid access",dim_lin,line_index+line_length-1);
			}
			
			Matrix2D ret_value(line_length,column_length);
			for(register uint i = 0; i < line_length; ++i){
				ret_value.value[i]->setSelection(*value[line_index+i]);
			}
			return ret_value;
		};
		void Matrix2D::setSelection(const uint &line_index, const uint &column_index, const Matrix2D &part){
			if(line_index < 0){
				throw OutOfBoundsException("Invalid access",dim_lin,line_index);
			}
			if(line_index+part.dim_lin > dim_lin){
				throw OutOfBoundsException("Invalid access",dim_lin,line_index+part.dim_lin-1);
			}
			
			for(register uint i = 0; i < part.dim_lin; ++i){
				value[line_index+i]->setSelection(column_index,*(part.value[i]));
			}
		};
		
			// -operators-
		
		Matrix2D& Matrix2D::operator -- (){
			if(dim_lin != dim_col){
				throw OutOfBoundsException(
						"Transponding non square matrix.",
						Min(dim_lin,dim_col),
						Max(dim_lin,dim_col)
					);
			}
		
			double aux = 0.0;
			for(register uint i = 0; i < dim_lin; ++i){
				for(register uint j = i+1; j < dim_lin; ++j){ // square matrix
					aux = (*this)[i][j];
					(*this)[i][j] = (*this)[j][i];
					(*this)[j][i] = aux;
				}
			}
			return (*this);
		};
		Matrix2D& Matrix2D::operator = (const Matrix2D &mat){
			if(dim_lin != mat.dim_lin){
				throw OutOfBoundsException(
						"Atribuicao Invalida. Matrices must have same dimensions.",
						Min(dim_lin,mat.dim_lin),
						Max(dim_lin,mat.dim_lin)
					);
			}
			
			copy(mat);
			return (*this);
		};
		Matrix2D Matrix2D::operator + (const Matrix2D &mat) const {
			if(dim_lin != mat.dim_lin){
				throw OutOfBoundsException(
						"Invalid operation. Matrices must have same dimensions.",
						Min(dim_lin,mat.dim_lin),
						Max(dim_lin,mat.dim_lin)
					);
			}
			
			Matrix2D result(dim_lin,dim_col);
			for(register uint i = 0; i < dim_lin; ++i){
				(*(result.value[i])) = (*value[i]) + (*(mat.value[i]));
			}
			return result;
		};
		Matrix2D& Matrix2D::operator += (const Matrix2D &mat){
			if(dim_lin != mat.dim_lin){
				throw OutOfBoundsException(
						"Invalid operation. Matrices must have same dimensions.",
						Min(dim_lin,mat.dim_lin),
						Max(dim_lin,mat.dim_lin)
					);
			}
			
			for(register uint i = 0; i < dim_lin; ++i){
				(*value[i]) += (*(mat.value[i]));
			}
			return (*this);
		};
		Matrix2D Matrix2D::operator - (const Matrix2D &mat) const {
			if(dim_lin != mat.dim_lin){
				throw OutOfBoundsException(
						"Invalid operation. Matrices must have same dimensions.",
						Min(dim_lin,mat.dim_lin),
						Max(dim_lin,mat.dim_lin)
					);
			}
			
			Matrix2D result(dim_lin,dim_col);
			for(register uint i = 0; i < dim_lin; ++i){
				(*(result.value[i])) = (*value[i]) - (*(mat.value[i]));
			}
			return result;
		};
		Matrix2D& Matrix2D::operator -= (const Matrix2D &mat){
			if(dim_lin != mat.dim_lin){
				throw OutOfBoundsException(
						"Invalid operation. Matrices must have same dimensions.",
						Min(dim_lin,mat.dim_lin),
						Max(dim_lin,mat.dim_lin)
					);
			}
			
			for(register uint i = 0; i < dim_lin; ++i){
				(*value[i]) -= (*(mat.value[i]));
			}
			return (*this);
		};
		Matrix2D Matrix2D::operator * (const Matrix2D &mat) const {
			if(dim_col != mat.dim_lin){
				throw OutOfBoundsException(
					"Invalid access",
					Min(dim_col,mat.dim_lin),
					Max(dim_col,mat.dim_lin));
			}
			
			Matrix2D result(dim_lin,mat.dim_col);
			for(register uint i = 0; i < dim_lin; ++i){
				for(register uint k = 0; k < dim_col; ++k){
					for(register uint j = 0; j < mat.dim_col; ++j){ // trocado aumenta a eficiencia
						result[i][j] += (*this)[i][k] * mat[k][j];
					}
				}
			}
			return result;
		};
		Matrix2D Matrix2D::operator * (const double &scalar) const {
			Matrix2D result(dim_lin,dim_col);
			for(register uint i = 0; i < dim_lin; ++i){
				(*(result.value[i])) = (*value[i]) * scalar;
			}
			return result;
		};
		Matrix2D& Matrix2D::operator *= (const double &scalar){
			for(register uint i = 0; i < dim_lin; ++i){
				(*value[i]) *= scalar;
			}
			return (*this);
		};
		Matrix2D Matrix2D::operator / (const double &scalar) const {
			if(scalar == 0.0){
				throw DivisionByZeroException<Matrix2D>("Divisao Invalida!", (*this));
			}
			
			Matrix2D result(dim_lin,dim_col);
			for(register uint i = 0; i < dim_lin; ++i){
				(*(result.value[i])) = (*value[i]) / scalar;
			}
			return result;
		};
		Matrix2D& Matrix2D::operator /= (const double &scalar){
			if(scalar == 0.0){
				throw DivisionByZeroException<Matrix2D>("Divisao Invalida!", (*this));
			}
			
			for(register uint i = 0; i < dim_lin; ++i){
				(*value[i]) /= scalar;
			}
			return (*this);
		};
		Matrix2D& Matrix2D::operator () (const uint &begin_index, const uint &end_index) {
			if(begin_index > end_index){ // if dumb user
				return (*this)(end_index,begin_index);
				//return this->operator()(end_index,begin_index);
			}
			if(end_index >= dim_col){
				throw OutOfBoundsException("Invalid access",dim_col,end_index);
			}
			
			if(copied){
				dim_col = end_index - begin_index + 1;
				for(register uint i = 0; i < dim_lin; ++i){
					value[i] = &( (*value[i])(begin_index, end_index) );
				}
				return *this;
			}
			else{
				Matrix2D *temp = new Matrix2D(dim_lin);
				temp->dim_col = end_index - begin_index + 1;
				temp->copied = true;
				for(register uint i = 0; i < dim_lin; ++i){
					temp->value[i] = &( (*value[i])(begin_index, end_index) );
				}
				return *temp;
			}
			return *this;
			
		};
		Matrix2D& Matrix2D::operator , (const double &num) {
			if(dim_lin == 0){
				throw OutOfBoundsException("Invalid access",dim_lin,0);
			}
		
			if(copied){
				value[0] = &( (*value[0]) , num );
				return *this;
			}
			else{
				Matrix2D *temp = new Matrix2D(dim_lin);
				temp->dim_col = dim_col;
				temp->copied = true;
				temp->value[0] = &( (*value[0]) , num );
				for(register uint i = 1; i < dim_lin; ++i){
					temp->value[i] = value[i];
				}
				return *temp;
			}
		
		};
		Matrix2D& Matrix2D::operator , (const MatrixManipulation &action) {
		
			if(action == Matrix::pr){
				if(value[0]->getDim() == 0){
					throw OutOfBoundsException("Invalid access",dim_lin,0);
				}
				
				if(copied){
					value[0] = &( (*value[0]) , action );
					return *this;
				}
				else{
					Matrix2D *temp = new Matrix2D(dim_lin);
					temp->dim_col = dim_col;
					temp->copied = true;
					temp->value[0] = &( (*value[0]) , action );
					for(register uint i = 1; i < dim_lin; ++i){
						temp->value[i] = value[i];
					}
					return *temp;
				}
			}
			else if(action == Matrix::nl){
				if(copied){
					--dim_lin;
					if( value[0]->getCopied() ){
						delete value[0];
					}
					value = &( value[1] );
					return *this;
				}
				else{
					Matrix2D *temp = new Matrix2D(dim_lin-1);
					temp->dim_col = dim_col;
					temp->copied = true;
					for(register uint i = 1; i < dim_lin; ++i){
						temp->value[i-1] = value[i];
					}
					return *temp;
				}
			}
			return *this;
		};
		Matrix1D& Matrix2D::operator [] (const uint &index) const {
			if(index < 0 || index > dim_lin){
				throw OutOfBoundsException("Invalid access",dim_lin,index);
			}
			
			return (*value[index]);
		};
		
			// -metodos internos publicos-
		Matrix2D Matrix2D::transp() const {
			Matrix2D result(dim_col,dim_lin);
			for(register uint i = 0; i < dim_lin; ++i){
				for(register uint j = 0; j < value[i]->getDim(); ++j){
					result[j][i] = (*this)[i][j];
				}
			}
			return result;
		};
		
			// -read & write-
		string Matrix2D::toS() const {
			string ret_value = "[\n";
			for(register uint i = 0; i < dim_lin; ++i){
				ret_value += value[i]->toS() + "\n";
			}
			return ret_value + "]";
		};
			
		bool Matrix2D::filePrint( FILE *parq ) const {
			return fprintf( parq, "%s", toS().c_str() ) == 1;
		};

		double* Matrix2D::toPointer() const {
			double *mat = NULL;
			mat = (double*) malloc( dim_lin*dim_col*sizeof(double) );
			if(mat == NULL){
				cout << "Couldn't allocate memmory!" << endl;
				exit(-1);
			}
			for(register uint i = 0; i < dim_lin; ++i)
				for(register uint j = 0; j < dim_col; ++j)
					mat[i*dim_col+j] = (*(value[i]))[j];
			return mat;
		};
		double** Matrix2D::toDoublePointer() const {
			double **mat = NULL;
			mat = (double**) malloc( dim_lin*sizeof(double*) );
			if(mat == NULL){
				cout << "Couldn't allocate memmory!" << endl;
				exit(-1);
			}
			for(register uint i = 0; i < dim_lin; ++i)
				mat[i] = value[i]->toPointer();
			return mat;
		};
			// -image-
			
			// -test-
		void Matrix2D::test(){
			cout << endl << "---Test:Matrix2D---" << endl;
			
			using Matrix::pr;
			using Matrix::nl;
			
			Matrix2D mx1(5,4);
			Matrix2D mx2(5,4);
			
			mx1,
				1.0, 2.0, pr, -5.0, nl,
				nl,
				0.4, 1.1, 2.5, nl,
				nl,
				5.0, pr, pr, 5.5;
				
			cout << "MX1: " << mx1 << pendl;
			cout << "MX1(1,3): " << mx1(1,3) << pendl;
			cout << "MX1(1,3)(1,1): " << mx1(1,3)(1,1) << pendl;
			cout << "MX1(1,3)(1,1).transp(): " << mx1(1,3)(1,1).transp() << pendl;
			cout << "MX1(1,3).transp()(1,3).transp(): " << mx1(1,3).transp()(1,3).transp() << pendl;
			
			mx2(0,3),
				-1.0, 2.5, pr, 1.3, nl,
				7.0, pr, 2.0, nl,
				2.1, 1.5, -3.0, nl;
			
			cout << "MX2: " << mx2 << pendl;
			
			cout << "MX1(0,2) + MX2(1,3): " << mx1(0,2) + mx2(1,3) << endl;
			
			cout << "MX1 + MX2: " << (mx1 + mx2) << pendl;
			cout << "MX1 - MX2: " << (mx1 - mx2) << pendl;
			cout << "MX1 * 2.0: " << mx1 * 2.0 << pendl;
			cout << "MX1: " << mx1 << pendl;
			cout << "2.0 * MX1: " << 2.0 * mx1 << pendl;
			cout << "MX1: " << mx1 << pendl;
			cout << "MX1 /= 5.0: " << (mx1 /= 5.0) << pendl;
			cout << "MX1: " << mx1 << pendl;
			cout << "MX2 *= 2.0: " << (mx2 *= 2.0) << pendl;
			cout << "MX2: " << mx2 << pendl;
			
			cout << "MX2[1]: " << mx2[1] << pendl;
			cout << "MX2[1][1]: " << mx2[1][1] << pendl;
			
			try{
				cout << "MT2[7]: " << mx2[7] << pendl;
			}
			catch(OutOfBoundsException &exc){
				cout << endl << "Exception:" << endl;
				cout << exc << pendl;
				cout << "Expected value " << 0.0 << eendl << endl;
			}
			catch(DivisionByZeroException< Matrix2D > &exc){
				cout << endl << "Exception:" << endl;
				cout << exc << pendl;
				cout << "Tending to " << (exc.getNum() / 0.001) << eendl << endl;
			}
			catch(...){
				cout << endl << "Exception:" << endl;
				cout << "Unknown!" << endl << endl;
			}
			
			cout << "MX1: " << mx1 << endl;
			cout << "MX1[2]: " << mx1[2] << endl;
			cout << "MX1[2][2]: " << mx1[2][2] << endl;
			
			Matrix1D mat1(5);
			
			mat1, 1,2,3,4,5;
			
			cout << endl << "Test1: " << mat1 << endl;
			
			cout << endl << "Test2: " << Matrix2D( mat1 ) << endl;
		
			cout << endl << "Test3: " << Matrix2D( mat1 ).transp() << endl;
		
			cout << endl << "Test4: " << Matrix2D( mat1 ).transp() * Matrix2D( mat1 ) << endl;
		
			cout << endl << "Test5: " << Matrix2D( mat1 ).transp() * mat1 << endl; // auto coercion
			
			try{
				cout << endl << "Test6: " << Matrix2D( mat1 ).transp() * mat1(1,2) << endl;
			}
			catch(OutOfBoundsException &exc){
				cout << endl << "Exception:" << endl;
				cout << exc << pendl;
				cout << "Expected value " << 0.0 << eendl << endl;
			}
			catch(DivisionByZeroException< Matrix2D > &exc){
				cout << endl << "Exception:" << endl;
				cout << exc << pendl;
				cout << "Tending to " << (exc.getNum() / 0.001) << eendl << endl;
			}
			catch(...){
				cout << endl << "Exception:" << endl;
				cout << "Unknown!" << endl << endl;
			}
		
			cout << endl << "Test7: " << mat1 * Matrix2D( mat1 ).transp() << endl; // auto coercion
			
			cout << endl << "---TestEnd---" << endl;
		};
			
			// -external operators-
			
			// -external methods-
			
		// declaring static variable's memory position
		uint Matrix2D::next_id = 0;
		
	};
};
